// Package mobsql is an interface for downloading and loading one or multiple
// GTFS archives (pulled from the Mobility Database catalog) into a SQLite
// database.
//
// The database (seeded through mobsql) mirrors GTFS's specification
// (e.g. there is a transfers, stop_times, stops, agency tables etc.); however
// each GTFS table also has an additional 'source' column. The source column
// refers to Mobility Database mdb_id field. Besides that, GTFS schema is
// imported as 1-to-1 for GTFS schedule specification sans several exceptions
// (such as stop_times conversion of departure_time to int). Exceptions can be
// seen by examining the dbschema package's Schema variable (and noting
// Conversion fields set on LoadColumn specs).
//
// There are 4 primary functions exposed by mobsql's API: Load, Status, Purge,
// and Compute. Each of these functions operates on a "SourcesetFilter". The
// purpose of the SourcesetFilter is that rather then manually specifying
// mdb_id's to address, through the SourcesetFilter, the user can narrow down
// the Mobility Database catalog down by geography, bounding box, a glob
// search, max km distance, and other parameters. An uninitialized
// SourcesetFilter (e.g. default values) passes through *all* sources.
package mobsql

import (
	"encoding/json"
	"fmt"
	"strings"

	"git.sr.ht/~mil/mobsql/app"
	"git.sr.ht/~mil/mobsql/logger"
	"git.sr.ht/~mil/mobsql/utilfiles"
)

// Config contains the configuration for the Mobsql application passed
// on each operation.
type Config struct {
	SQLiteDBPath *string // Filesystem path for the SQLite DB, if nil or "" uses default of ~/.cache/mobsql/sqlite.db
	LogInfo      bool    // Whether to enable info messages
	LogWarn      bool    // Whether to enable warning messages
	LogDebug     bool    // Whether to enable debug messages
}

// SourcesetFilter represents the filter that is used to match against
// Mobility Database catalog sources allowing specifying mdbid's, geography,
// globs, bounding box matches, and similar.
//
// Aliased from underlying app package SourcesetFilter.
type SourcesetFilter = app.SourcesetFilter

// SourcesetOpResult represents the result of an Compute, Purge, or Load
// request, indicating whether the operation was successful and the
// effected mdbids.

// Aliased from underlying app package SourcesetOpResult.
type SourcesetOpResult = app.SourcesetOpResult

// SourceStatus represents metadata about a particular source.
//
// Aliased from underlying app package SourceStatus.
type SourceStatus = app.SourceStatus

// Status queries the database for sources that match the given sourceset.
// This is a read-only operation which can be used to determine the effected
// sources for subsequent requests with the same sourceset.
func Status(filter SourcesetFilter, config *Config) ([]SourceStatus, error) {
	return oneshot(filter, config, app.SourcesetStatus)
}

// Load downloads the associated GTFS zip archives from the MobilityDB's
// URL; and then performs an import, pulling each GTFS Zip CSV file into
// the local SQLite database. Note that this operation is cached (e.g.
// subsequent requests with the same source will skip tables already imported
// if underlying GTFS CSV file checksums are unchanged).
func Load(filter SourcesetFilter, config *Config) (*SourcesetOpResult, error) {
	return oneshot(filter, config, app.SourcesetLoad)
}

// Compute recomputes the application's computed tables (contrived from the
// source loaded GTFS tables) specified in application's configuration. Each
// computed table is based on a underlying view (see config). The concept of
// a computed table itself is similar to concept of a SQL materialized view;
// however 'recomputation' occurs on a per-source level. So you don't need
// to 'rematerialize' an entire table.
func Compute(filter SourcesetFilter, config *Config) (*SourcesetOpResult, error) {
	return oneshot(filter, config, app.SourcesetCompute)
}

// Purge removes (deletes) all associated data from underlying GTFS tables
// in the SQLite database for the given sourceset.
func Purge(filter SourcesetFilter, config *Config) (*SourcesetOpResult, error) {
	return oneshot(filter, config, app.SourcesetPurge)
}

// FilterToSourceset translates the input filter into an array of sourceset
// ids (e.g. in current implementation this is directly mdbids) by querying
// the internal view _vsources which is based on _mdb (which represents an
// import of the Mobility Database catalog).
func FilterToSourceset(m *app.MDB, filter SourcesetFilter) ([]int, error) {
	var (
		sourcesMdbids   = []int{}
		whereCond, args = app.FilterBuildWhereClause(filter)
		query           = fmt.Sprintf(`select mdbid from _vsources %s`, whereCond)
	)
	if err := m.DB.Select(&sourcesMdbids, query, args...); err != nil {
		return nil, err
	} else {
		filterBytes, _ := json.MarshalIndent(filter, "", "  ")
		filtersString := strings.Replace(string(filterBytes), "\n", "", -1)
		logger.Infof("Applied filter (%v) matched %d Mobility DB GTFS sources: %v", filtersString, len(sourcesMdbids), sourcesMdbids)
		return sourcesMdbids, nil
	}
}

// oneshot is a wrapper function used by Load, Compute, Purge, and Delete
// to make the public interface for these functions simpler.
//
// This function implements the following logic:
// 1) opens the database
// 2) translates the given filters into mdbids,
// 3) runs the given callback function againast the mdbids
// 4) closes the db
// 5) returns the result
func oneshot[T any](
	filter SourcesetFilter,
	config *Config,
	runfunc func(m *app.MDB, mdbids []int) (T, error),
) (T, error) {
	var (
		returnValue T
		dbPath      string
	)

	// Set log messages based on config
	if config != nil {
		if config.LogInfo {
			logger.LogInfo = true
		}
		if config.LogWarn {
			logger.LogWarn = true
		}
		if config.LogDebug {
			logger.LogDebug = true
		}
	}

	// Determine DB path
	if config != nil && config.SQLiteDBPath != nil && *config.SQLiteDBPath != "" {
		dbPath = *config.SQLiteDBPath
	} else {
		dbPath = utilfiles.CacheDir("") + "sqlite.db"
	}

	if m, err := app.InitializeApp(dbPath); err != nil {
		return returnValue, err
	} else if mdbids, err := FilterToSourceset(m, filter); err != nil {
		return returnValue, err
	} else if result, err := runfunc(m, mdbids); err != nil {
		return returnValue, err
	} else {
		m.DB.Close()
		return result, err
	}
}
